#!/usr/bin/env node

const fs = require('fs').promises;

// ============================================================================

const DIR_PROTO = "src/proto";

/** @type {string} */
var NS;

/** @type { {[prefix: string]: {files: string[], msgs: {key: string, id: number, name: string}[]}} } */
var proto_info = {
    // c_s: {
    //     files: [],
    //     msgs: [
    //         {key: 'c', id: 1, name: ''},
    //     ],
    // }
};

/** @type { {[key: string]: {files: string[], svcs: {name: string, lcname: string, methods: {name: string, req: string, res: string}[]}[]}} } */
var rpc_info = {
    // gs: {
    //     files: [],
    //     svcs: [
    //         {
    //             name: '',
    //             lcname: '',
    //             methods: [
    //                 {name: '', req: '', res: ''},
    //             ],
    //         },
    //     ],
    // },
};

/** @type {string[]} */
var shell_cmds = [
    'set -e',
    'GOGO_DIR=$(go list -f={{.Dir}} -m github.com/gogo/protobuf)',
];

// ============================================================================

async function mkdir(dir) {
    await fs.mkdir(dir, {recursive: true, mode: 0o755});
}

async function rm(fn) {
    await fs.rm(fn, {recursive: true, force: true});
}

async function rm_files_prefix(dir, prefix) {
    let a = (await fs.readdir(dir)).filter(v => v.startsWith(prefix));
    for (let fn of a) {
        await rm(`${dir}/${fn}`);
    }
}

async function file_exists(fn) {
    try {
        await fs.access(fn)
        return true;
    } catch {
        return false;
    }
}

async function read_file(fn) {
    return await fs.readFile(fn, 'utf8');
}

async function write_file(fn, data) {
    await fs.writeFile(fn, data.toString ? data.toString() : data);
}

async function exec(cmd) {
    return new Promise((rv, rj) => {
        require('child_process').spawn(cmd, {
            shell: '/bin/sh',
            stdio: 'inherit',
        }).on('close', code => {
            if (code == 0) {
                rv(code);
            } else {
                rj(code);
            }
        });
    });
}

// ============================================================================

class TextBuffer {
    constructor() {
        this.arr = [];
    }

    printf(indent, s) {
        if (indent == undefined) {
            this.arr.push('');
        } else {
            this.arr.push(' '.repeat(indent * 4) + s);
        }
    }

    toString() {
        return this.arr.join('\n');
    }
}

// ============================================================================

async function init_NS() {
    NS = (await read_file('go.mod')).match(/^module (\w+)/)[1];
}

// ============================================================================

async function init_proto_info() {
    let protos = (await fs.readdir(DIR_PROTO)).filter(v => v.endsWith('.proto')).sort();

    for (let fn of protos) {
        let m = fn.match(/^([a-zA-Z]+_[a-zA-Z]+)\./);
        if (!m) continue;

        let prefix = m[1];

        // get info
        let info = proto_info[prefix];
        if (!info) {
            info = {files: [], msgs: []};
            proto_info[prefix] = info;
        }

        // add file
        info.files.push(fn);

        // add msgs
        let text = await read_file(`${DIR_PROTO}/${fn}`);
        for (let [_, name, key, id] of text.matchAll(/message\s+(([a-zA-Z]+)_\w+)\s*\{\s*\/\/\s*msgid:\s*(\d+)/g)) {
            info.msgs.push({key: key.toLowerCase(), id, name});
        }
    }
}

async function gen_proto(svr, conns) {
    await gen_proto_msg_file        (svr, conns);
    await gen_proto_msg_dt          (svr, conns);
    await gen_proto_msg_mappings    (svr, conns);
    await gen_proto_handler_dt      (svr, conns);
    await gen_proto_handler_mappings(svr, conns);
    await gen_proto_handlers        (svr, conns);
}

async function gen_proto_msg_file(svr, conns) {
    let outdir = `src/${svr}/msg`;

    await rm(outdir);
    await mkdir(outdir);

    let files = [];
    for (let [prefix] of conns) {
        files = files.concat(proto_info[prefix].files);
    }

    // gen
    shell_cmds.push(
        `echo "generating proto for ${svr}"`,
        `protoc -I=$GOGO_DIR/gogoproto/ -I=$GOGO_DIR/protobuf/ -I=src/proto --gogofaster_out=${outdir} ${files.join(' ')}`,
    );
}

async function gen_proto_msg_dt(svr, conns) {
    let buf = new TextBuffer();

    buf.printf(0, "package msg");
    buf.printf();
    buf.printf(0, "type Message interface {");
    buf.printf(0, "    MsgId() uint32");
    buf.printf(0, "    Marshal() ([]byte, error)");
    buf.printf(0, "    Unmarshal([]byte) error");
    buf.printf(0, "}");
    buf.printf();
    buf.printf(0, "func Marshal(m Message) ([]byte, error) {");
    buf.printf(0, "    return m.Marshal()");
    buf.printf(0, "}");
    buf.printf();
    buf.printf(0, "func Unmarshal(b []byte, m Message) error {");
    buf.printf(0, "    return m.Unmarshal(b)");
    buf.printf(0, "}");
    buf.printf();

    await write_file(`src/${svr}/msg/message.go`, buf);
}

async function gen_proto_msg_mappings(svr, conns) {
    let buf = new TextBuffer();

    buf.printf(0, "package msg");
    buf.printf();

    // msg creators
    buf.printf(0, "var MsgCreators = map[uint32]func() Message{");
    for (let [prefix] of conns) {
        for (let msg of proto_info[prefix].msgs) {
            buf.printf(1, `${msg.id}: func() Message {`);
            buf.printf(1, `    return &${msg.name}{}`);
            buf.printf(1, "},");
        }
    }
    buf.printf(0, "}");
    buf.printf();

    // msg ids
    for (let [prefix] of conns) {
        for (let msg of proto_info[prefix].msgs) {
            buf.printf(0, `func (self *${msg.name}) MsgId() uint32 {`);
            buf.printf(0, `    return ${msg.id}`);
            buf.printf(0, '}');
            buf.printf();
        }
    }

    // save
    await write_file(`src/${svr}/msg/idstruct.go`, buf);
}

async function gen_proto_handler_dt(svr, conns) {
    let buf = new TextBuffer();

    buf.printf(0, "package msg");
    buf.printf();
    buf.printf(0, "var MsgHandlers = map[uint32]func(message Message, ctx interface{}){}");
    buf.printf();
    buf.printf(0, "func Handler(msgid uint32, h func(message Message, ctx interface{})) {");
    buf.printf(0, "    MsgHandlers[msgid] = h");
    buf.printf(0, "}");
    buf.printf();

    await write_file(`src/${svr}/msg/handler.go`, buf);
}

async function gen_proto_handler_mappings(svr, conns) {
    let buf = new TextBuffer();

    // imports
    let imports = [];
    for (let [prefix, key] of conns) {
        if (key) {
            imports.push(`${NS}/src/${svr}/handler/${prefix}`);
        }
    }
    imports.push(`${NS}/src/${svr}/msg`);
    imports.sort();

    // print
    buf.printf(0, "package handler");
    buf.printf();

    buf.printf(0, "import (");
    for (let v of imports) {
        buf.printf(0, `    "${v}"`);
    }
    buf.printf(0, ")");
    buf.printf();

    buf.printf(0, "func Init() {");
    for (let [prefix, key] of conns) {
        for (let msg of proto_info[prefix].msgs) {
            if (msg.key == key) {
                buf.printf(1, `msg.Handler(${msg.id}, ${prefix}.${msg.name})`);
            }
        }
    }
    buf.printf(0, "}");
    buf.printf();

    // save
    await mkdir(`src/${svr}/handler`);
    await write_file(`src/${svr}/handler/init.go`, buf);
}

async function gen_proto_handlers(svr, conns) {
    for (let [prefix, key] of conns) {
        if (!key) continue;

        await mkdir(`src/${svr}/handler/${prefix}`);

        for (let msg of proto_info[prefix].msgs) {
            if (msg.key == key) {
                let outfile = `src/${svr}/handler/${prefix}/${msg.name}.go`;
                if (! await file_exists(outfile)) {
                    let buf = new TextBuffer();

                    buf.printf(0, `package ${prefix}`);
                    buf.printf();
                    buf.printf(0, "import (");
                    buf.printf(0, `    "${NS}/src/${svr}/msg"`);
                    buf.printf(0, ")");
                    buf.printf();
                    buf.printf(0, `func ${msg.name}(message msg.Message, ctx interface{}) {`);
                    buf.printf(0, `    req := message.(*msg.${msg.name})`);
                    buf.printf(0, "    req = req");
                    buf.printf(0, "}");
                    buf.printf();

                    await write_file(outfile, buf);
                }
            }
        }
    }
}

// ============================================================================

async function init_rpc_info() {
    let protos = (await fs.readdir(DIR_PROTO)).filter(v => v.startsWith('svc.') && v.endsWith('.proto')).sort();

    for (let fn of protos) {
        let m = fn.match(/^svc\.([a-zA-Z]+)\./);
        if (!m) continue;

        let key = m[1];

        // get info
        let info = rpc_info[key];
        if (!info) {
            info = {files: [], svcs: []};
            rpc_info[key] = info;
        }

        // add file
        info.files.push(fn);

        // add service
        let text = await read_file(`${DIR_PROTO}/${fn}`);
        let n = 0;
        let svc;
        for (let [_, name] of text.matchAll(/service\s+(\w+)\s*\{/g)) {
            n++;
            if (n > 1) throw 'only 1 service per proto file is allowed';

            svc = {name, lcname: name.toLowerCase(), methods: []};
            info.svcs.push(svc);
        }

        // add service methods
        for (let [_, name, req, res] of text.matchAll(/rpc\s+(\w+)\s*\(\s*(\w+)\s*\)\s*returns\s*\(\s*(\w+)\s*\)/g)) {
            svc.methods.push({name, req, res});
        }
    }
}

async function gen_rpc(svr, skey, ckeys) {
    await gen_rpc_msg_file       (svr, skey, ckeys);
    await gen_rpc_server_reg     (svr, skey, ckeys);
    await gen_rpc_server_handlers(svr, skey, ckeys);
    await gen_rpc_clients        (svr, skey, ckeys);
}

async function gen_rpc_msg_file(svr, skey, ckeys) {
    let outdir = `src/${svr}/msg`;

    await rm_files_prefix(outdir, 'svc.');
    await mkdir(outdir);

    let files = [];
    for (let key of ckeys.concat(skey)) {
        files = files.concat(rpc_info[key].files);
    }

    // gen
    shell_cmds.push(
        `echo "generating rpc for ${svr}"`,
        `protoc -I=$GOGO_DIR/gogoproto/ -I=$GOGO_DIR/protobuf/ -I=src/proto --gogofaster_out=plugins=grpc:${outdir} ${files.join(' ')}`,
    );
}

async function gen_rpc_server_reg(svr, skey, ckeys) {
    let buf = new TextBuffer();

    // imports
    let imports = [];
    for (let svc of rpc_info[skey].svcs) {
        imports.push(`${NS}/src/${svr}/rpc/grs/${svc.lcname}`);
    }
    imports.push(`${NS}/src/${svr}/msg`);
    imports.sort();

    // print
    buf.printf(0, "package grs");
    buf.printf();

    buf.printf(0, "import (");
    for (let v of imports) {
        buf.printf(0, `    "${v}"`);
    }
    buf.printf();
    buf.printf(0, `    "google.golang.org/grpc"`);
    buf.printf(0, ")");
    buf.printf();

    buf.printf(0, "func register(gsvr *grpc.Server) {");
    for (let svc of rpc_info[skey].svcs) {
        buf.printf(1, `msg.Register${svc.name}Server(gsvr, &${svc.lcname}.Service{})`);
    }
    buf.printf(0, "}");
    buf.printf();

    // save
    await mkdir(`src/${svr}/rpc/grs`);
    await write_file(`src/${svr}/rpc/grs/reg.go`, buf);
}

async function gen_rpc_server_handlers(svr, skey, ckeys) {
    for (let svc of rpc_info[skey].svcs) {
        await mkdir(`src/${svr}/rpc/grs/${svc.lcname}`);

        // service.go
        {
            let outfile = `src/${svr}/rpc/grs/${svc.lcname}/service.go`;
            if (! await file_exists(outfile)) {
                let buf = new TextBuffer();

                buf.printf(0, `package ${svc.lcname}`);
                buf.printf();
                buf.printf(0, "type Service struct {");
                buf.printf(0, "}");
                buf.printf();

                await write_file(outfile, buf);
            }
        }

        // methods
        for (let m of svc.methods) {
            let outfile = `src/${svr}/rpc/grs/${svc.lcname}/${m.name}.go`;
            if (! await file_exists(outfile)) {
                let buf = new TextBuffer();

                buf.printf(0, `package ${svc.lcname}`);
                buf.printf();
                buf.printf(0, "import (");
                buf.printf(0, `    "context"`);
                buf.printf(0, `    "${NS}/src/${svr}/msg"`);
                buf.printf(0, ")");
                buf.printf();
                buf.printf(0, `func (self *Service) ${m.name}(ctx context.Context, req *msg.${m.req}) (res *msg.${m.res}, err error) {`);
                buf.printf(0, "    return");
                buf.printf(0, "}");
                buf.printf();

                await write_file(outfile, buf);
            }
        }
    }
}

async function gen_rpc_clients(svr, skey, ckeys) {
    let buf = new TextBuffer();

    buf.printf(0, `package grc`);
    buf.printf();
    buf.printf(0, "import (");
    buf.printf(0, `    "${NS}/src/${svr}/msg"`);
    buf.printf();
    buf.printf(0, `    "google.golang.org/grpc"`);
    buf.printf(0, ")");
    buf.printf();

    // types
    for (let key of ckeys) {
        buf.printf(0, `type RpcClient${key.toUpperCase()} struct {`);
        for (let svc of rpc_info[key].svcs) {
            buf.printf(1, `${svc.name} msg.${svc.name}Client`);
        }
        buf.printf(0, "}");
        buf.printf()
    }

    // new
    for (let key of ckeys) {
        let uckey = key.toUpperCase();

        buf.printf(0, `func newRpcClient${uckey}(cc *grpc.ClientConn) *RpcClient${uckey} {`);
        buf.printf(0, `    return &RpcClient${uckey}{`);
        for (let svc of rpc_info[key].svcs) {
            buf.printf(2, `${svc.name}: msg.New${svc.name}Client(cc),`);
        }
        buf.printf(0, "    }");
        buf.printf(0, "}");
        buf.printf();
    }

    // save
    await mkdir(`src/${svr}/rpc/grc`);
    await write_file(`src/${svr}/rpc/grc/clients.go`, buf);
}

// ============================================================================

(async () => {
    console.log();
    console.log('scanning protos');

    // init
    await init_NS();
    await init_proto_info();
    await init_rpc_info();

    // gen protos
    await gen_proto("gate", [
        ["c_gw",  "c" ],
        ["gw_gs", "gs"],
    ]);

    await gen_proto("game", [
        ["gw_gs",  "gw"],
        ["c_gs",   "c" ],
    ]);

    await gen_proto("bot", [
        ["c_gw",  "gw"],
        ["c_gs",  "gs" ],
    ]);

    // post shell commands
    await exec(shell_cmds.join('\n'));

    // done
    console.log();
})().catch(console.error);
